--[[-----------------------------------------------------------------------------
GSE-TreeGroup based on TreeGroup Container
Container that uses a tree control to switch between groups.  Idenitcal to TreeGroup but with Bigger fonts.
-------------------------------------------------------------------------------]]
local Type, Version = "GSE-TreeGroup", 3
local AceGUI = LibStub and LibStub("AceGUI-3.0", true)
if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then
    return
end

-- Lua APIs
local next, pairs, ipairs, assert, type = next, pairs, ipairs, assert, type
local math_min, math_max, floor = math.min, math.max, math.floor
local select, tremove, unpack, tconcat = select, table.remove, unpack, table.concat

-- WoW APIs
local CreateFrame, UIParent = CreateFrame, UIParent

-- Recycling functions
local new, del
do
    local pool = setmetatable({}, {__mode = "k"})
    function new()
        local t = next(pool)
        if t then
            pool[t] = nil
            return t
        else
            return {}
        end
    end
    function del(t)
        for k in pairs(t) do
            t[k] = nil
        end
        pool[t] = true
    end
end

local DEFAULT_TREE_WIDTH = 175
local DEFAULT_TREE_SIZABLE = true

--[[-----------------------------------------------------------------------------
Support functions
-------------------------------------------------------------------------------]]
local function GetButtonUniqueValue(line)
    local parent = line.parent
    if parent and parent.value then
        return GetButtonUniqueValue(parent) .. "\001" .. line.value
    else
        return line.value
    end
end

local function UpdateButton(button, treeline, selected, canExpand, isExpanded)
    local self = button.obj
    local toggle = button.toggle
    local text = treeline.text or ""
    local icon = treeline.icon
    local iconCoords = treeline.iconCoords
    local level = treeline.level
    local value = treeline.value
    local uniquevalue = treeline.uniquevalue
    local disabled = treeline.disabled

    button.treeline = treeline
    button.value = value
    button.uniquevalue = uniquevalue
    if selected then
        button:LockHighlight()
        button.selected = true
    else
        button:UnlockHighlight()
        button.selected = false
    end
    button.level = level
    if (level == 1) then
        button:SetNormalFontObject("GameFontNormalLarge")
        button:SetHighlightFontObject("GameFontHighlightLarge")
        button.text:SetPoint("LEFT", (icon and 16 or 0) + 8, 2)
    elseif (level == 2) then
        button:SetNormalFontObject("GameFontNormalLarge")
        button:SetHighlightFontObject("GameFontHighlightLarge")
        button.text:SetPoint("LEFT", (icon and 16 or 0) + 8 * level, 2)
    else
        button:SetNormalFontObject("GameFontHighlight")
        button:SetHighlightFontObject("GameFontHighlight")
        button.text:SetPoint("LEFT", (icon and 16 or 0) + 8 * level, 2)
    end

    if disabled then
        button:EnableMouse(false)
        button.text:SetText("|cff808080" .. text .. FONT_COLOR_CODE_CLOSE)
    else
        button.text:SetText(text)
        button:EnableMouse(true)
    end

    if icon then
        button.icon:SetTexture(icon)
        button.icon:SetPoint("LEFT", 8 * level, (level == 1) and 0 or 1)
    else
        button.icon:SetTexture(nil)
    end

    if iconCoords then
        button.icon:SetTexCoord(unpack(iconCoords))
    else
        button.icon:SetTexCoord(0, 1, 0, 1)
    end

    if canExpand then
        if not isExpanded then
            toggle:SetNormalTexture(130838) -- Interface\\Buttons\\UI-PlusButton-UP
            toggle:SetPushedTexture(130836) -- Interface\\Buttons\\UI-PlusButton-DOWN
        else
            toggle:SetNormalTexture(130821) -- Interface\\Buttons\\UI-MinusButton-UP
            toggle:SetPushedTexture(130820) -- Interface\\Buttons\\UI-MinusButton-DOWN
        end
        toggle:Show()
    else
        toggle:Hide()
    end
end

local function ShouldDisplayLevel(tree)
    local result = false
    for _, v in ipairs(tree) do
        if v.children == nil and v.visible ~= false then
            result = true
        elseif v.children then
            result = result or ShouldDisplayLevel(v.children)
        end
        if result then
            return result
        end
    end
    return false
end

local function addLine(self, v, tree, level, parent)
    local line = new()
    line.value = v.value
    line.text = v.text
    line.icon = v.icon
    line.iconCoords = v.iconCoords
    line.disabled = v.disabled
    line.tree = tree
    line.level = level
    line.parent = parent
    line.visible = v.visible
    line.uniquevalue = GetButtonUniqueValue(line)
    if v.children then
        line.hasChildren = true
    else
        line.hasChildren = nil
    end
    self.lines[#self.lines + 1] = line
    return line
end

--fire an update after one frame to catch the treeframes height
local function FirstFrameUpdate(frame)
    local self = frame.obj
    frame:SetScript("OnUpdate", nil)
    self:RefreshTree(nil, true)
end

local function BuildUniqueValue(...)
    local n = select("#", ...)
    if n == 1 then
        return ...
    else
        return (...) .. "\001" .. BuildUniqueValue(select(2, ...))
    end
end

--[[-----------------------------------------------------------------------------
Scripts
-------------------------------------------------------------------------------]]
local function Expand_OnClick(frame)
    local button = frame.button
    local self = button.obj
    local status = (self.status or self.localstatus).groups
    status[button.uniquevalue] = not status[button.uniquevalue]
    self:RefreshTree()
end

local function Button_OnClick(frame)
    local self = frame.obj
    self:Fire("OnClick", frame.uniquevalue, frame.selected)
    if not frame.selected then
        self:SetSelected(frame.uniquevalue)
        frame.selected = true
        frame:LockHighlight()
        Expand_OnClick(frame.toggle)
    else
        local mbutton = GetMouseButtonClicked()
        if mbutton == "RightButton" then
            self:Fire("OnGroupSelected", frame.uniquevalue)
        end
    end
    AceGUI:ClearFocus()
end

local function Button_OnDoubleClick(button)
    local self = button.obj
    local status = (self.status or self.localstatus).groups
    status[button.uniquevalue] = not status[button.uniquevalue]
    self:RefreshTree()
end

local function Button_OnEnter(frame)
    local self = frame.obj
    self:Fire("OnButtonEnter", frame.uniquevalue, frame)

    if self.enabletooltips then
        local tooltip = AceGUI.tooltip
        tooltip:SetOwner(frame, "ANCHOR_NONE")
        tooltip:ClearAllPoints()
        tooltip:SetPoint("LEFT", frame, "RIGHT")
        tooltip:SetText(frame.text:GetText() or "", 1, .82, 0, true)

        tooltip:Show()
    end
end

local function Button_OnLeave(frame)
    local self = frame.obj
    self:Fire("OnButtonLeave", frame.uniquevalue, frame)

    if self.enabletooltips then
        AceGUI.tooltip:Hide()
    end
end

local function OnScrollValueChanged(frame, value)
    if frame.obj.noupdate then
        return
    end
    local self = frame.obj
    local status = self.status or self.localstatus
    status.scrollvalue = floor(value + 0.5)
    self:RefreshTree()
    AceGUI:ClearFocus()
end

local function Tree_OnSizeChanged(frame)
    frame.obj:RefreshTree()
end

local function Tree_OnMouseWheel(frame, delta)
    local self = frame.obj
    if self.showscroll then
        local scrollbar = self.scrollbar
        local min, max = scrollbar:GetMinMaxValues()
        local value = scrollbar:GetValue()
        local newvalue = math_min(max, math_max(min, value - delta))
        if value ~= newvalue then
            scrollbar:SetValue(newvalue)
        end
    end
end

local function Dragger_OnLeave(frame)
    frame:SetBackdropColor(1, 1, 1, 0)
end

local function Dragger_OnEnter(frame)
    frame:SetBackdropColor(1, 1, 1, 0.8)
end

local function Dragger_OnMouseDown(frame)
    local treeframe = frame:GetParent()
    treeframe:StartSizing("RIGHT")
end

local function Dragger_OnMouseUp(frame)
    local treeframe = frame:GetParent()
    local self = treeframe.obj
    local treeframeParent = treeframe:GetParent()
    treeframe:StopMovingOrSizing()
    --treeframe:SetScript("OnUpdate", nil)
    treeframe:SetUserPlaced(false)
    --Without this :GetHeight will get stuck on the current height, causing the tree contents to not resize
    treeframe:SetHeight(0)
    treeframe:ClearAllPoints()
    treeframe:SetPoint("TOPLEFT", treeframeParent, "TOPLEFT", 0, 0)
    treeframe:SetPoint("BOTTOMLEFT", treeframeParent, "BOTTOMLEFT", 0, 0)

    local status = self.status or self.localstatus
    status.treewidth = treeframe:GetWidth()

    treeframe.obj:Fire("OnTreeResize", treeframe:GetWidth())
    -- recalculate the content width
    treeframe.obj:OnWidthSet(status.fullwidth)
    -- update the layout of the content
    treeframe.obj:DoLayout()
end

--[[-----------------------------------------------------------------------------
Methods
-------------------------------------------------------------------------------]]
local methods = {
    ["OnAcquire"] = function(self)
        self:SetTreeWidth(DEFAULT_TREE_WIDTH, DEFAULT_TREE_SIZABLE)
        self:EnableButtonTooltips(true)
        self.frame:SetScript("OnUpdate", FirstFrameUpdate)
    end,
    ["OnRelease"] = function(self)
        self.status = nil
        self.tree = nil
        self.frame:SetScript("OnUpdate", nil)
        for k, v in pairs(self.localstatus) do
            if k == "groups" then
                for k2 in pairs(v) do
                    v[k2] = nil
                end
            else
                self.localstatus[k] = nil
            end
        end
        self.localstatus.scrollvalue = 0
        self.localstatus.treewidth = DEFAULT_TREE_WIDTH
        self.localstatus.treesizable = DEFAULT_TREE_SIZABLE
    end,
    ["EnableButtonTooltips"] = function(self, enable)
        self.enabletooltips = enable
    end,
    ["CreateButton"] = function(self)
        local num = AceGUI:GetNextWidgetNum("TreeGroupButton")
        local button =
            CreateFrame("Button", ("AceGUI30TreeButton%d"):format(num), self.treeframe, "OptionsListButtonTemplate")
        button.obj = self

        local icon = button:CreateTexture(nil, "OVERLAY")
        icon:SetWidth(14)
        icon:SetHeight(14)
        button.icon = icon

        button:SetScript("OnClick", Button_OnClick)
        button:SetScript("OnDoubleClick", Button_OnDoubleClick)
        button:SetScript("OnEnter", Button_OnEnter)
        button:SetScript("OnLeave", Button_OnLeave)

        button.toggle.button = button
        button.toggle:SetScript("OnClick", Expand_OnClick)

        button.text:SetHeight(14) -- Prevents text wrapping

        return button
    end,
    ["SetStatusTable"] = function(self, status)
        assert(type(status) == "table")
        self.status = status
        if not status.groups then
            status.groups = {}
        end
        if not status.scrollvalue then
            status.scrollvalue = 0
        end
        if not status.treewidth then
            status.treewidth = DEFAULT_TREE_WIDTH
        end
        if status.treesizable == nil then
            status.treesizable = DEFAULT_TREE_SIZABLE
        end
        self:SetTreeWidth(status.treewidth, status.treesizable)
        self:RefreshTree()
    end,
    --sets the tree to be displayed
    ["SetTree"] = function(self, tree, filter)
        self.filter = filter
        if tree then
            assert(type(tree) == "table")
        end
        self.tree = tree
        self:RefreshTree()
    end,
    ["BuildLevel"] = function(self, tree, level, parent)
        local groups = (self.status or self.localstatus).groups

        for _, v in ipairs(tree) do
            if v.children then
                if not self.filter or ShouldDisplayLevel(v.children) then
                    local line = addLine(self, v, tree, level, parent)
                    if groups[line.uniquevalue] then
                        self:BuildLevel(v.children, level + 1, line)
                    end
                end
            elseif v.visible ~= false or not self.filter then
                addLine(self, v, tree, level, parent)
            end
        end
    end,
    ["RefreshTree"] = function(self, scrollToSelection, fromOnUpdate)
        local buttons = self.buttons
        local lines = self.lines

        for _, v in ipairs(buttons) do
            v:Hide()
        end
        while lines[1] do
            local t = tremove(lines)
            for k in pairs(t) do
                t[k] = nil
            end
            del(t)
        end

        if not self.tree then
            return
        end
        --Build the list of visible entries from the tree and status tables
        local status = self.status or self.localstatus
        local groupstatus = status.groups
        local tree = self.tree

        local treeframe = self.treeframe

        status.scrollToSelection = status.scrollToSelection or scrollToSelection -- needs to be cached in case the control hasn't been drawn yet (code bails out below)

        self:BuildLevel(tree, 1)

        local numlines = #lines

        local maxlines = (floor(((self.treeframe:GetHeight() or 0) - 20) / 18))
        if maxlines <= 0 then
            return
        end

        if self.frame:GetParent() == UIParent and not fromOnUpdate then
            self.frame:SetScript("OnUpdate", FirstFrameUpdate)
            return
        end

        local first, last

        scrollToSelection = status.scrollToSelection
        status.scrollToSelection = nil

        if numlines <= maxlines then
            --the whole tree fits in the frame
            status.scrollvalue = 0
            self:ShowScroll(false)
            first, last = 1, numlines
        else
            self:ShowScroll(true)
            --scrolling will be needed
            self.noupdate = true
            self.scrollbar:SetMinMaxValues(0, numlines - maxlines)
            --check if we are scrolled down too far
            if numlines - status.scrollvalue < maxlines then
                status.scrollvalue = numlines - maxlines
            end
            self.noupdate = nil
            first, last = status.scrollvalue + 1, status.scrollvalue + maxlines
            --show selection?
            if scrollToSelection and status.selected then
                local show
                for i, line in ipairs(lines) do -- find the line number
                    if line.uniquevalue == status.selected then
                        show = i
                    end
                end
                if not show then
                    -- selection was deleted or something?
                elseif show >= first and show <= last then
                    -- all good
                else
                    -- scrolling needed!
                    if show < first then
                        status.scrollvalue = show - 1
                    else
                        status.scrollvalue = show - maxlines
                    end
                    first, last = status.scrollvalue + 1, status.scrollvalue + maxlines
                end
            end
            if self.scrollbar:GetValue() ~= status.scrollvalue then
                self.scrollbar:SetValue(status.scrollvalue)
            end
        end

        local buttonnum = 1
        for i = first, last do
            local line = lines[i]
            local button = buttons[buttonnum]
            if not button then
                button = self:CreateButton()

                buttons[buttonnum] = button
                button:SetParent(treeframe)
                button:SetFrameLevel(treeframe:GetFrameLevel() + 1)
                button:ClearAllPoints()
                if buttonnum == 1 then
                    if self.showscroll then
                        button:SetPoint("TOPRIGHT", -22, -10)
                        button:SetPoint("TOPLEFT", 0, -10)
                    else
                        button:SetPoint("TOPRIGHT", 0, -10)
                        button:SetPoint("TOPLEFT", 0, -10)
                    end
                else
                    button:SetPoint("TOPRIGHT", buttons[buttonnum - 1], "BOTTOMRIGHT", 0, 0)
                    button:SetPoint("TOPLEFT", buttons[buttonnum - 1], "BOTTOMLEFT", 0, 0)
                end
            end

            UpdateButton(
                button,
                line,
                status.selected == line.uniquevalue,
                line.hasChildren,
                groupstatus[line.uniquevalue]
            )
            button:Show()
            buttonnum = buttonnum + 1
        end
    end,
    ["SetSelected"] = function(self, value)
        local status = self.status or self.localstatus
        if status.selected ~= value then
            status.selected = value
            self:Fire("OnGroupSelected", value)
        end
    end,
    ["Select"] = function(self, uniquevalue, ...)
        self.filter = false
        local status = self.status or self.localstatus
        local groups = status.groups
        local path = {...}
        for i = 1, #path do
            groups[tconcat(path, "\001", 1, i)] = true
        end
        status.selected = uniquevalue
        self:RefreshTree(true)
        self:Fire("OnGroupSelected", uniquevalue)
    end,
    ["SelectByPath"] = function(self, ...)
        self:Select(BuildUniqueValue(...), ...)
    end,
    ["SelectByValue"] = function(self, uniquevalue)
        self:Select(uniquevalue, ("\001"):split(uniquevalue))
    end,
    ["ShowScroll"] = function(self, show)
        self.showscroll = show
        if show then
            self.scrollbar:Show()
            if self.buttons[1] then
                self.buttons[1]:SetPoint("TOPRIGHT", self.treeframe, "TOPRIGHT", -22, -10)
            end
        else
            self.scrollbar:Hide()
            if self.buttons[1] then
                self.buttons[1]:SetPoint("TOPRIGHT", self.treeframe, "TOPRIGHT", 0, -10)
            end
        end
    end,
    ["OnWidthSet"] = function(self, width)
        local content = self.content
        local treeframe = self.treeframe
        local status = self.status or self.localstatus
        status.fullwidth = width

        local contentwidth = width - status.treewidth - 20
        if contentwidth < 0 then
            contentwidth = 0
        end
        content:SetWidth(contentwidth)
        content.width = contentwidth

        local maxtreewidth = math_min(400, width - 50)

        if maxtreewidth > 100 and status.treewidth > maxtreewidth then
            self:SetTreeWidth(maxtreewidth, status.treesizable)
        end
        if treeframe.SetResizeBounds then
            treeframe:SetResizeBounds(100, 1, maxtreewidth, 1600)
        else
            treeframe:SetMaxResize(maxtreewidth, 1600)
        end
    end,
    ["OnHeightSet"] = function(self, height)
        local content = self.content
        local contentheight = height - 20
        if contentheight < 0 then
            contentheight = 0
        end
        content:SetHeight(contentheight)
        content.height = contentheight
    end,
    ["SetTreeWidth"] = function(self, treewidth, resizable)
        if not resizable then
            if type(treewidth) == "number" then
                resizable = false
            elseif type(treewidth) == "boolean" then
                resizable = treewidth
                treewidth = DEFAULT_TREE_WIDTH
            else
                resizable = false
                treewidth = DEFAULT_TREE_WIDTH
            end
        end
        self.treeframe:SetWidth(treewidth)
        self.dragger:EnableMouse(resizable)

        local status = self.status or self.localstatus
        status.treewidth = treewidth
        status.treesizable = resizable

        -- recalculate the content width
        if status.fullwidth then
            self:OnWidthSet(status.fullwidth)
        end
    end,
    ["GetTreeWidth"] = function(self)
        local status = self.status or self.localstatus
        return status.treewidth or DEFAULT_TREE_WIDTH
    end,
    ["LayoutFinished"] = function(self, width, height)
        if self.noAutoHeight then
            return
        end
        self:SetHeight((height or 0) + 20)
    end
}

--[[-----------------------------------------------------------------------------
Constructor
-------------------------------------------------------------------------------]]
local PaneBackdrop = {
    bgFile = "Interface\\ChatFrame\\ChatFrameBackground",
    edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
    tile = true,
    tileSize = 16,
    edgeSize = 16,
    insets = {left = 3, right = 3, top = 5, bottom = 3}
}

local DraggerBackdrop = {
    bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
    edgeFile = nil,
    tile = true,
    tileSize = 16,
    edgeSize = 1,
    insets = {left = 3, right = 3, top = 7, bottom = 7}
}

local function Constructor()
    local num = AceGUI:GetNextWidgetNum(Type)
    local frame = CreateFrame("Frame", nil, UIParent)

    local treeframe = CreateFrame("Frame", nil, frame, "BackdropTemplate")
    treeframe:SetPoint("TOPLEFT")
    treeframe:SetPoint("BOTTOMLEFT")
    treeframe:SetWidth(DEFAULT_TREE_WIDTH)
    treeframe:EnableMouseWheel(true)
    treeframe:SetBackdrop(PaneBackdrop)
    treeframe:SetBackdropColor(0.1, 0.1, 0.1, 0.5)
    treeframe:SetBackdropBorderColor(0.4, 0.4, 0.4)
    treeframe:SetResizable(true)
    if treeframe.SetResizeBounds then -- WoW 10.0
        treeframe:SetResizeBounds(100, 1, 400, 1600)
    else
        treeframe:SetMinResize(100, 1)
        treeframe:SetMaxResize(400, 1600)
    end
    treeframe:SetScript("OnUpdate", FirstFrameUpdate)
    treeframe:SetScript("OnSizeChanged", Tree_OnSizeChanged)
    treeframe:SetScript("OnMouseWheel", Tree_OnMouseWheel)

    local dragger = CreateFrame("Frame", nil, treeframe, "BackdropTemplate")
    dragger:SetWidth(8)
    dragger:SetPoint("TOP", treeframe, "TOPRIGHT")
    dragger:SetPoint("BOTTOM", treeframe, "BOTTOMRIGHT")
    dragger:SetBackdrop(DraggerBackdrop)
    dragger:SetBackdropColor(1, 1, 1, 0)
    dragger:SetScript("OnEnter", Dragger_OnEnter)
    dragger:SetScript("OnLeave", Dragger_OnLeave)
    dragger:SetScript("OnMouseDown", Dragger_OnMouseDown)
    dragger:SetScript("OnMouseUp", Dragger_OnMouseUp)

    local scrollbar =
        CreateFrame(
        "Slider",
        ("AceConfigDialogTreeGroup%dScrollBar"):format(num),
        treeframe,
        "UIPanelScrollBarTemplate"
    )
    scrollbar:SetScript("OnValueChanged", nil)
    scrollbar:SetPoint("TOPRIGHT", -10, -26)
    scrollbar:SetPoint("BOTTOMRIGHT", -10, 26)
    scrollbar:SetMinMaxValues(0, 0)
    scrollbar:SetValueStep(1)
    scrollbar:SetValue(0)
    scrollbar:SetWidth(16)
    scrollbar:SetScript("OnValueChanged", OnScrollValueChanged)

    local scrollbg = scrollbar:CreateTexture(nil, "BACKGROUND")
    scrollbg:SetAllPoints(scrollbar)
    scrollbg:SetColorTexture(0, 0, 0, 0.4)

    local border = CreateFrame("Frame", nil, frame, "BackdropTemplate")
    border:SetPoint("TOPLEFT", treeframe, "TOPRIGHT")
    border:SetPoint("BOTTOMRIGHT")
    border:SetBackdrop(PaneBackdrop)
    border:SetBackdropColor(0.1, 0.1, 0.1, 0.5)
    border:SetBackdropBorderColor(0.4, 0.4, 0.4)

    --Container Support
    local content = CreateFrame("Frame", nil, border)
    content:SetPoint("TOPLEFT", 10, -10)
    content:SetPoint("BOTTOMRIGHT", -10, 10)

    local widget = {
        frame = frame,
        lines = {},
        levels = {},
        buttons = {},
        hasChildren = {},
        localstatus = {groups = {}, scrollvalue = 0},
        filter = false,
        treeframe = treeframe,
        dragger = dragger,
        scrollbar = scrollbar,
        border = border,
        content = content,
        type = Type
    }
    for method, func in pairs(methods) do
        widget[method] = func
    end
    treeframe.obj, dragger.obj, scrollbar.obj = widget, widget, widget

    return AceGUI:RegisterAsContainer(widget)
end

AceGUI:RegisterWidgetType(Type, Constructor, Version)
